So as stated in the previous tutorial, these first few lessons are going to cover a decent amount, so we can get to the more exciting parts of OpenGL and GLSL. In this tutorial, we will be positioning a cube and rendering it in 3d space with the help of the glm math library. The source code for the tutorial can be accessed here.
Setting Up the Cube
The first thing we need is to set up the vertices of the cube. As we can see from the diagram we will need 8 vertices for our cube as seen in the following code. With each vertex we also associate a color so that the colors interpolate across the faces of the cube (Note: this is not the way models/ 3d figures are normally given color but to keep focus on just getting a cube rendered we’ll use this for today).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//cube vertices //position color GLfloat cube_vertices[]= { 1.f, 1.f, 1.f, 1.0f, 0.0f, 0.0f, //0 -1.f, 1.f, 1.f, 0.0f, 1.0f, 0.0f, //1 -1.f, 1.f, -1.f, 0.0f, 0.0f, 1.0f, //2 1.f, 1.f, -1.f, 1.0f, 1.0f, 1.0f, //3 1.f, -1.f, 1.f, 1.0f, 1.0f, 0.0f, //4 -1.f, -1.f, 1.f, 1.0f, 1.0f, 1.0f, //5 -1.f, -1.f, -1.f, 0.0f, 1.0f, 1.0f, //6 1.f, -1.f, -1.f, 1.0f, 0.0f, 1.0f //7 }; |
Next we need to setup the indices for our vertices. Since objects in OpenGL are commonly rendered with triangles, to render each of the faces of the cube we must specify the two triangles that make it up. That means we will need 2 for the top face, 2 for the bottom, 2 for the left, 2 for the right, 2 for the front and 2 for the back; 12 triangles in total.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//indices for the cube // 6 faces , 2 triangles per face, 12 triangles GLuint indices[] = { 0, 1, 3, //top 1 3, 1, 2, //top 2 2, 6, 7, //front 1 7, 3, 2, //front 2 7, 6, 5, //bottom 1 5, 4, 7, //bottom 2 5, 1, 4, //back 1 4, 1, 0, //back 2 4, 3, 7, //right 1 3, 4, 0, //right 2 5, 6, 2, //left 1 5, 1, 2 //left 2 }; |
After we setup our Array Buffer and Element Array Buffer, we setup our camera by creating the view and projection matrices. But before we get into those two simple lines of code let’s talk about the GLM Math library which will help us do that.
The GLM Math Library
GLM (OpenGL Mathematics) is a mathematics library that offers a suite of math functions and classes for use along with GLSL. This library is made to work with perfectly OpenGL and GLSL programs so this is suitable for us to use for the creation of our scenes.
In this tutorial, we are going to use GLM to just set up our cube’s position and orientation in the scene and render the cube from it’s 3D space to its 2D space on screen. The vec3 (a vector with 3 values x, y, and z) and mat4 (a 4×4 matrix) data types are used in this application but there are many more that will be of use to us as we continue our work with OpenGL.
The View and Projection Matrices
There are two matrices which are crucial to us being able to render a 3D scene. These are the View and Projection Matrices.
The View Matrix works as our camera, giving a view of the scene. Wherever we position our matrix and whatever location it looks at effects what will be scene from this view if we choose to use it to express our view space (more on this in a bit, when we talk about the model view projection matrix). To setup the View Matrix, we use the glm function lookAt. The function takes three glm::vec3 as input. The first being the camera’s location; the second where the camera is pointed at; and the third, the vector that states which way is up in relation to the camera.
1 2 3 |
View = glm::lookAt(vec3(2.5f, 2.f, 3.f), vec3(0.0f, 0.0f, 0.f), vec3(0.0f, 1.0f, 0.0f)); |
Here the camera is setup at position (2.5f, 2.f, 3.f); looks at the point (0.f, 0.f, 0.f); and the vector (0.f, 1.f, 0.f) is up in relation to it.
With the View Matrix setup we now have a camera looking into our scene. To transform our 3D scene to a 2D screen we need another matrix. This is where the Projection Matrix comes in. The projection matrix can be either to give a perspective view of a scene or an orthographic view of the scene. Here we do a perspective projection of the scene to our screen. In perspective projection, objects further back in a scene appear farther away. The other choice is orthographic and that makes objects appear as the same size no matter their distance from the camera.
1 2 3 4 |
Projection = glm::perspective(45.f, (float)SCREEN_WIDTH / SCREEN_HEIGHT, 0.1f, 1000.f); |
Using glm, to setup our projection matrix we use the perspective function. This function takes four values for input. The first is the field of view. The field of view is the vertical angle through which we look at the world with the camera. The second is the ratio of our window size, so everything appears as it should regardless of the window width and height. With an improper ratio in relation to our window size, objects in the scene may appear stretched. The last two are our near and far clipping planes, specifying how close and how far a piece of geometry must be to the camera to be rendered. (Note: Do not use 0.f for your near clipping plane this will create undesired effects. Essentially all values/distances from the camera would map to 0 because of this).
To make sure that we are rendering only rendering geometry closest to the camera, we use glEnable(GL_DEPTH_TEST). Without it geometry hidden by other geometry or in this case back parts of our cube, may appear on our screen, giving our scene an unusual appearance as can be seen in the image below.
The Shaders
Each shader is now placed in its own file, Shader.vs (Vertex Shader) and Shader.fs(Fragment Shader), and are loaded using the LoadSourceFromFile function. There is a slight tweak to our shader code from the previous tutorials since we are now dealing with 3D geometry that will be rendered onto a 2D surface, which will be discussed in a bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#version 430 in vec3 VertexPosition; in vec3 VertexColor; out vec3 VFragColor; uniform mat4 MVP; void main() { VFragColor= VertexColor; vec4 v= vec4(VertexPosition, 1); gl_Position= MVP * v; } |
1 2 3 4 5 6 7 8 9 10 |
#version 430 in vec3 VFragColor; out vec4 FragColor; void main() { FragColor = vec4(VFragColor, 1.0); } |
Uniforms
We are also introduced to uniforms in this tutorial. Essentially, uniforms are variables passed to the shader program, which remain constant through a glDrawArrays or glDrawElements call. Uniforms are commonly used for but definitely are not limited to a model’s Normal, Model, ModelViewProjection matrices, a model’s material and textures, a scene’s lights, etc. In this tutorial, our only uniform is the 4×4 Model View Projection matrix.
In our shader, declaring a uniform is simple. All we have to do is use the uniform keyword before declaring the variable.
1 |
uniform mat4 MVP; |
To load a value into that uniform variable, in our application we must find the location of our uniform variable and then load the variable which we wish to use. To do that, we use the glGetUniformLocation function to find the uniform’s location and is returned through a GLuint value, which in this application we load into uMVP. Next we tell OpenGL what value we want to load at that location.
1 2 |
GLuint uMVP = glGetUniformLocation(ProgramID, "MVP"); glUniformMatrix4fv(uMVP, 1, GL_FALSE, &MVP[0][0]); |
To send our MVP matrix to our shader, we use glUniformMatrix4fv, which loads a 4×4 matrix to the uniform location. (Note: There are other functions which we will look at in future tutorials available to load ints, floats, and vectors of varying sizes. They all follow the common form of glUnifom… where the ellipsis are replaced with qualifiers that specify the type of data we are loading.) The first variable of this function specifies the location. The second says how many of this type of variable we are loading at this location. Specific to uniform matrices, we then specify if the matrix should be transposed. It is GL_FALSE in this case because glm handles our matrices in the same column major format used by GLSL. Finally, we load our matrix by specifying the location of value at index [0][0] in our matrix.
So what are we doing with this Model View Projection Matrix?
Firstly, the Model View Projection (MVP) Matrix is a combination of the transformations from one coordinate space to another. The Model Matrix expresses the model’s position and orientation in world space. The View Matrix, as we setup earlier, is where the camera is positioned and what it is looking at. And finally the Projection Matrix, the conversion from what the camera sees to screen space. The combination of these takes the vertices of our model (in this case the cube) and converts them to their “screen space” based on the model’s world location and orientation and the camera’s location and orientation. This is why in our vertex shader we take the MVP Matrix and multiply it by the vertex position and set it to gl_Position. (As I write this I have realize that some of readers may not have relevant experience with linear algebra for this to make complete sense. I plan on writing something up in the near future as a crash course in linear algebra topics relevant to computer graphics).
With all of this in mind, we can now build and run our OpenGL application. You should see a cube with colors interpolated between the cube’s vertices. In future tutorials, we will perform transformations on the vertices to rotate, translate and scale our matrix so it animates.