Tutorial 6-Matrices and multi-object loading in Opengl 3.3
Hello readers, In this article, we will learn how to port the 6th tutorial on matrices and multi-object loading in the new OpenGL 3.3. We saw in the last tutorial how to load a lit, textue mapped 3ds model in OpenGL 3.3 and above. With that knowledge in our hands, we can now explore on how to handle multiple objects with matrices. Since the first tutorial, we know that handling of geometry in OpenGL 3.0 and above requires the use of vertex buffer objects VBO along with their state management using the vertex array object (VAO). We already loaded the texture mapped 3ds mesh in the previous tutorial so the geometry handling for this tutorial is exactly the same as the last tutorial. Moreover, the lighting calculations are going to be exactly the same. The only things changed in this tutorial are the handling of multiple objects and matrices to position these objects individually. Here we will show you how to write a custom matrix library and how to use a stable matrix library like glm.
Contents
Handling multiple meshes
We learned in the previous tutorials how to load a 3ds mesh containing multiple arrays like vertices, normals, and texture coordinates. There we used a single vertex array object (VAO) and attached individual VBOs for vertices, normals and texture coordinates. For handling multiple meshes, we create multiple vaos with each mesh having its own vao. So the way the mesh is loaded and the vertex,normals, texture coordinate data is copied to the vbos is exactly the same. Now we just loop through all of the objects, bind the vao for that object and then copy the object's data:
//constant and ids cerated globally const int TOTAL_OBJECTS=3; GLuint vboVerticesID[TOTAL_OBJECTS], vboTexCoordID[TOTAL_OBJECTS], vboNormalsID[TOTAL_OBJECTS], vboIndicesID[TOTAL_OBJECTS], vaoID[TOTAL_OBJECTS]; //In init code //Create vao and vbo stuff glGenVertexArrays(TOTAL_OBJECTS, vaoID); glGenBuffers (TOTAL_OBJECTS, vboVerticesID); glGenBuffers (TOTAL_OBJECTS, vboTexCoordID); glGenBuffers (TOTAL_OBJECTS, vboNormalsID); glGenBuffers (TOTAL_OBJECTS, vboIndicesID); for(int i=0;i< TOTAL_OBJECTS;i++) { glBindVertexArray(vaoID[i]); glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID[i]); glBufferData (GL_ARRAY_BUFFER, sizeof(GLfloat)*3*object[i].vertices_qty, &object[i].vertex[0], GL_STATIC_DRAW); GL_CHECK_ERRORS glEnableVertexAttribArray(shader["vVertex"]); glVertexAttribPointer (shader["vVertex"], 3, GL_FLOAT, GL_FALSE,stride,0); GL_CHECK_ERRORS glBindBuffer (GL_ARRAY_BUFFER, vboTexCoordID[i]); glBufferData (GL_ARRAY_BUFFER, sizeof(GLfloat)*2*object[i].vertices_qty, &object[i].mapcoord[0], GL_STATIC_DRAW); GL_CHECK_ERRORS glEnableVertexAttribArray(shader["vUV"]); glVertexAttribPointer (shader["vUV"], 2, GL_FLOAT, GL_FALSE,sizeof(GLfloat)*2,0); GL_CHECK_ERRORS glBindBuffer (GL_ARRAY_BUFFER, vboNormalsID[i]); glBufferData (GL_ARRAY_BUFFER, sizeof(GLfloat)*3*object[i].vertices_qty, &object[i].normal[0], GL_STATIC_DRAW); GL_CHECK_ERRORS glEnableVertexAttribArray(shader["vNormal"]); glVertexAttribPointer (shader["vNormal"], 3, GL_FLOAT, GL_FALSE,stride,0); GL_CHECK_ERRORS glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID[i]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort)*3*object[i].polygons_qty, &object[i].polygon[0], GL_STATIC_DRAW); glBindVertexArray(0); }
For details of these calls, please refer to the earlier tutorial on loading 3ds mesh. On shutdown, we delete all of the allocated resources:
void OnShutdown() { for(int i=0;i < TOTAL_OBJECTS; i++){ if(object[i].id_texture!=-1) glDeleteTextures( 1, &object[i].id_texture); } glDeleteBuffers( TOTAL_OBJECTS, vboVerticesID); glDeleteBuffers( TOTAL_OBJECTS, vboTexCoordID); glDeleteBuffers( TOTAL_OBJECTS, vboNormalsID); glDeleteBuffers( TOTAL_OBJECTS, vboIndicesID); glDeleteVertexArrays( TOTAL_OBJECTS, vaoID); }
Handling of object matrices using GLM
We first look at how to handle matrices using the GLM library. The first thing we need to do is create the modelview matrix. Since every object has its own object to world matrix that places the object into world space. A legitimate question to ask here is that we have not specified the view matrix. The answer to this is we have implicitly done this when we defined the object to world matrix. We can initialze the MV matrix by calling the glm::mat4 constructor as follows:
glm::mat4 MV = glm::mat4(object[i].matrix[0][0],object[i].matrix[0][1], object[i].matrix[0][2], object[i].matrix[0][3], object[i].matrix[1][0],object[i].matrix[1][1], object[i].matrix[1][2], object[i].matrix[1][3], object[i].matrix[2][0],object[i].matrix[2][1], object[i].matrix[2][2], object[i].matrix[2][3], object[i].matrix[3][0],object[i].matrix[3][1], object[i].matrix[3][2], object[i].matrix[3][3]);
once we have the modelview matrix (MV), we obtain the normal matrix (N) from it by taking the inverse transpose of the modelview matrix as follows:
glm::mat3 N = glm::transpose(glm::inverse(glm::mat3(MV)));
Finally, we get the combined modelview projection matrix by concatenating the modelview (MV) and projection matrices:
glm::mat4 MVP = P*MV;
Note that we calcualted the projection matrix (P) in the resize handler as follows:
P = glm::perspective(45.0f, (GLfloat)w/h, 5.0f, 10000.f);
The parameters to this function are the same as those for gluPerspective. We will look into the internals of these functions in a later section.
Handling of object matrices using spacesimulator.net matrix library
The spacesimulator.net library contains a handy matrix library. Here we show how to calculate the modelview and projection matrices using this library. We make the current object's matrix as the current modelview matrix. We first calculate the inverse of this matrix and transpose it to obtain the normal matrix:
matrix_4x4_type N2,tmp, MVP; matrix_3x3_type N; MatrIdentity_4x4(N2); Fast_Inverse(object[i].matrix, tmp); MatrCopy_3x3_trsp(N2,tmp); MatrCopy_4x4_3x3(N2, N);
The function MatrIdentity_4x4 makes the given matrix an identity matrix. Next, we call the Fast_Inverse function that calculates the inverse of the given matrix. Next, we call the MatrCopy_3x3_trsp matrix function to transpose the upper 3x3 part of the given matrix. Finally, we use the MatrCopy_4x4_3x3 function to copy the upper 3x3 component of the given 4x4 matrix. Finally, we combine the modelview and projection matrices as follows:
MatrMul_4x4_4x4( object[i].matrix, P, MVP);
The Fast_Inverse function calculates the inverse using the approach given here. The idea is to dissect the 4x4 matrix into its 4 components. Then do the inverse of the rotation component (the upper 3x3 sub-matrix). Then do the dot product of each column to the translation component. This is done in the Fast_Inverse function given below:
void Fast_Inverse(matrix_4x4_type matrix, matrix_4x4_type inverse) { MatrIdentity_4x4(inverse); MatrCopy_3x3_trsp(inverse,matrix); float Rx[3]={matrix[0][0],matrix[1][0],matrix[2][0]}; float Ry[3]={matrix[0][1],matrix[1][1],matrix[2][1]}; float Rz[3]={matrix[0][2],matrix[1][2],matrix[2][2]}; float T[3]= {matrix[3][0], matrix[3][1],matrix[3][2]}; inverse[3][0] = -Dot(T,Rx); inverse[3][1] = -Dot(T,Ry); inverse[3][2] = -Dot(T,Rz); }
where Dot is the dot product calcualtion function. The perspective matrix (P) is calcualted in the resize handler by calling Perspective function as follows:
Perspective(45.0f, (GLfloat)w/h, 5.0f, 10000.f, P);
This function is defined as follows:
void Perspective(float fov, float asp, float zMin, float zMax, matrix_4x4_type matrix) { MatrIdentity_4x4(matrix); float yMax = zMin * tanf(fov*float(M_PI) / 360.0f); float yMin = -yMax; float xMin = yMin * asp; float xMax = -xMin; matrix[0][0]= (2.0f * zMin) / (xMax - xMin); matrix[1][1] = (2.0f * zMin) / (yMax - yMin); matrix[0][2] = (xMax + xMin) / (xMax - xMin); matrix[1][2] = (yMax + yMin) / (yMax - yMin); matrix[2][2] = -((zMax + zMin) / (zMax - zMin)); matrix[2][3] = -1.0f; matrix[3][2] = -((2.0f * (zMax*zMin))/(zMax - zMin)); matrix[3][3] = 0.0f; }
The details of this function can be found in any good basic computer graphics book or in the OpenGL programming guide.
Rendering of multiple meshes
For rendering multiple meshes, we loop through each mesh, assign its object to world matrix to obtain the modelview, normal and combined modelview projection matrices. Then, we pass these matrices to the shader:
for(int i=0;i< TOTAL_OBJECTS;i++) { //matrices setup code here #ifdef USE_GLM glUniformMatrix3fv(shader("N"), 1, GL_FALSE, glm::value_ptr(N)); glUniformMatrix4fv(shader("MV"), 1, GL_FALSE, glm::value_ptr(MV)); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(MVP)); #else glUniformMatrix3fv(shader("N"), 1, GL_FALSE, &N[0][0]); glUniformMatrix4fv(shader("MV"), 1, GL_FALSE, &object[i].matrix[0][0]); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, &MVP[0][0]); #endif
If the mesh has a texture, we bind that texture to the currently active texture unit. Since by default, GL_TEXTURE0 is active, we had already passed 0 to the textureMap uniform when we loaded the shader so this tells OpenGL that the sampler (textureMap) in the fragment shader refers to the texture bound to texture unit 0:
if (object[i].id_texture!=-1) { glBindTexture(GL_TEXTURE_2D, object[i].id_texture); // We set the active texture }
Since some meshes may not have any texture, in that case, we simply put the lighting contribution to the mesh. This we achieve by modifying the fragment shader slightly:
#version 330 smooth in vec2 vTexCoord; smooth in vec4 color; out vec4 vFragColor; uniform sampler2D textureMap; uniform bool has_texture; void main(void) { if(has_texture) vFragColor = texture(textureMap, vTexCoord)*color; else vFragColor = color; }
We now have a uniform boolean has_texture. This tells us if we have a texture map. If so we use the texture map other wise, we use the lighting shader only without the texture map. We set the uniform(has_texture) as follows:
glUniform1i(shader("has_texture"), object[i].id_texture!=-1);
For meshes without a texturemap, their texture id is -1. If a texture has texture, its id will not be -1. Finally, we bind the current mesh's vao and then call the DrawElements method as shown in the previous tutorials. Thats it for per vertex lighting in OpenGL3.3. We can choose whether we need GLM or the spacesimulator.net custom matrix library by defining USE_GLM at the start of the code as follows:
#define USE_GLM
Running the code gives us the following output:
SOURCE CODE
The Source Code of this lesson can be downloaded from the Tutorials Main Page